@barefootjs/blade 0.1.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.
Files changed (72) hide show
  1. package/README.md +73 -0
  2. package/dist/adapter/analysis/component-tree.d.ts +26 -0
  3. package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
  4. package/dist/adapter/blade-adapter.d.ts +537 -0
  5. package/dist/adapter/blade-adapter.d.ts.map +1 -0
  6. package/dist/adapter/boolean-result.d.ts +76 -0
  7. package/dist/adapter/boolean-result.d.ts.map +1 -0
  8. package/dist/adapter/emit-context.d.ts +105 -0
  9. package/dist/adapter/emit-context.d.ts.map +1 -0
  10. package/dist/adapter/expr/array-method.d.ts +74 -0
  11. package/dist/adapter/expr/array-method.d.ts.map +1 -0
  12. package/dist/adapter/expr/emitters.d.ts +176 -0
  13. package/dist/adapter/expr/emitters.d.ts.map +1 -0
  14. package/dist/adapter/expr/operand.d.ts +25 -0
  15. package/dist/adapter/expr/operand.d.ts.map +1 -0
  16. package/dist/adapter/index.d.ts +6 -0
  17. package/dist/adapter/index.d.ts.map +1 -0
  18. package/dist/adapter/index.js +189060 -0
  19. package/dist/adapter/lib/blade-naming.d.ts +128 -0
  20. package/dist/adapter/lib/blade-naming.d.ts.map +1 -0
  21. package/dist/adapter/lib/constants.d.ts +21 -0
  22. package/dist/adapter/lib/constants.d.ts.map +1 -0
  23. package/dist/adapter/lib/ir-scope.d.ts +48 -0
  24. package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
  25. package/dist/adapter/lib/types.d.ts +27 -0
  26. package/dist/adapter/lib/types.d.ts.map +1 -0
  27. package/dist/adapter/memo/seed.d.ts +84 -0
  28. package/dist/adapter/memo/seed.d.ts.map +1 -0
  29. package/dist/adapter/props/prop-classes.d.ts +34 -0
  30. package/dist/adapter/props/prop-classes.d.ts.map +1 -0
  31. package/dist/adapter/spread/spread-codegen.d.ts +72 -0
  32. package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
  33. package/dist/adapter/value/parsed-literal.d.ts +27 -0
  34. package/dist/adapter/value/parsed-literal.d.ts.map +1 -0
  35. package/dist/build.d.ts +28 -0
  36. package/dist/build.d.ts.map +1 -0
  37. package/dist/build.js +189080 -0
  38. package/dist/conformance-pins.d.ts +12 -0
  39. package/dist/conformance-pins.d.ts.map +1 -0
  40. package/dist/index.d.ts +9 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +189079 -0
  43. package/package.json +67 -0
  44. package/php/composer.json +32 -0
  45. package/php/src/BladeBackend.php +197 -0
  46. package/php/src/naming.php +84 -0
  47. package/php/tests/test_render.php +159 -0
  48. package/src/__tests__/blade-adapter-unit.test.ts +392 -0
  49. package/src/__tests__/blade-adapter.test.ts +52 -0
  50. package/src/__tests__/blade-counter.test.ts +63 -0
  51. package/src/__tests__/blade-query-href.test.ts +113 -0
  52. package/src/__tests__/blade-spread-attrs.test.ts +235 -0
  53. package/src/adapter/analysis/component-tree.ts +119 -0
  54. package/src/adapter/blade-adapter.ts +1912 -0
  55. package/src/adapter/boolean-result.ts +168 -0
  56. package/src/adapter/emit-context.ts +117 -0
  57. package/src/adapter/expr/array-method.ts +345 -0
  58. package/src/adapter/expr/emitters.ts +636 -0
  59. package/src/adapter/expr/operand.ts +35 -0
  60. package/src/adapter/index.ts +6 -0
  61. package/src/adapter/lib/blade-naming.ts +180 -0
  62. package/src/adapter/lib/constants.ts +33 -0
  63. package/src/adapter/lib/ir-scope.ts +83 -0
  64. package/src/adapter/lib/types.ts +30 -0
  65. package/src/adapter/memo/seed.ts +135 -0
  66. package/src/adapter/props/prop-classes.ts +66 -0
  67. package/src/adapter/spread/spread-codegen.ts +179 -0
  68. package/src/adapter/value/parsed-literal.ts +75 -0
  69. package/src/build.ts +37 -0
  70. package/src/conformance-pins.ts +86 -0
  71. package/src/index.ts +9 -0
  72. package/src/test-render.ts +782 -0
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Blade/PHP identifier, hash-key, and member/index-access conventions for
3
+ * the Blade adapter.
4
+ *
5
+ * Ported from `packages/adapter-twig/src/adapter/lib/twig-naming.ts`
6
+ * (`twigIdent`/`twigHashKey`/`twigLoopBindingAccessor`), adjusted for the
7
+ * real syntax divergences between Twig and Blade that matter here:
8
+ *
9
+ * 1. **Every variable reference needs the `$` sigil.** Twig has no sigil —
10
+ * `twigIdent(name)` returned the bare (possibly mangled) name, used
11
+ * directly as both a read and a `{% set %}` assignment target. Blade
12
+ * compiles straight to raw PHP, where every variable — read OR write —
13
+ * is `$name`. `bladeIdent(name)` (this file) mirrors `twigIdent`'s
14
+ * mangling-only role (used for the PHP array KEY a prop is threaded
15
+ * through as, e.g. `naming.php`'s `blade_ident`); `bladeVar(name)` is the
16
+ * NEW wrapper that additionally prepends `$` — every place the Blade
17
+ * adapter emits a bare variable REFERENCE (identifier read, loop-binding
18
+ * `@php($x = ...)` target, hash-entry value, …) uses `bladeVar`, not
19
+ * `bladeIdent` alone.
20
+ * 2. **Hash-literal keys are always quoted, but the literal itself is a PHP
21
+ * array, not a Twig hash.** `{'k': v}` → `['k' => v]` (mapping table) —
22
+ * `bladeHashKey` still always quotes (same bareword-key trap Twig/Jinja
23
+ * have), but every CALL SITE joins entries with `=>` and wraps them in
24
+ * `[...]`, never `{...}` / `: `.
25
+ * 3. **Member/index access has no raw-PHP polymorphic equivalent.** Twig's
26
+ * `.`/`[]` transparently resolve an object property, an array key, OR a
27
+ * getter method on ANY of {stdClass, array, null} under
28
+ * `strict_variables: false`. Raw PHP has no single operator with that
29
+ * property — `$x->prop` fatals on a non-object, `$x['key']` warns on
30
+ * `null`/scalar, and dynamic keys need yet another form. This adapter
31
+ * uses Laravel's `data_get($target, $key)` (from `illuminate/support`,
32
+ * already a transitive dependency of `illuminate/view`) uniformly for
33
+ * BOTH member access (`a.b` → `data_get($a, 'b')`) and index access
34
+ * (`a[i]` → `data_get($a, $i)`) — verified empirically (see
35
+ * `bladeMemberAccess`'s docstring) to be null-safe over the same
36
+ * {stdClass, array, null} value shapes Twig's dot/bracket cover, with NO
37
+ * runtime helper added to `packages/adapter-php` (the `data_get`
38
+ * lowering point is entirely on the TS emit side).
39
+ * 4. **Reserved-word set is COMPLETELY DIFFERENT, and for a different
40
+ * reason.** Twig keywords (`for`, `filter`, `if`, …) collide with Twig's
41
+ * OWN expression grammar — irrelevant here, since Blade variables are
42
+ * real PHP variables (`$for`, `$filter` are legal). What collides
43
+ * instead is anything the Blade/illuminate RENDER-TIME PHP SCOPE already
44
+ * binds to a different meaning, or a name PHP itself forbids — see
45
+ * `RESERVED_WORDS`'s docstring below and `php/src/naming.php`'s (this
46
+ * adapter's PHP-side twin, which MUST mirror this list exactly; each
47
+ * side carries a parity test against the other).
48
+ */
49
+
50
+ import type { LoopBindingPathSegment } from '@barefootjs/jsx'
51
+
52
+ /**
53
+ * Escape a string for a PHP/Blade single-quoted literal: backslash first (so
54
+ * it doesn't double-escape the quote we add next), then the quote. PHP's
55
+ * single-quoted string escaping rules are `\\` and `\'` ONLY (verified:
56
+ * `php -r "echo 'a\\'b';"` → `a'b`) — byte-identical to Twig's own
57
+ * single-quoted escaping, so this function's body is unchanged from
58
+ * `escapeTwigSingleQuoted`.
59
+ */
60
+ export function escapeBladeSingleQuoted(s: string): string {
61
+ return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")
62
+ }
63
+
64
+ /**
65
+ * Quote an array-literal KEY for Blade/PHP. Always quotes — a bareword key
66
+ * in a PHP array literal (`[key => value]`) is a syntax error, not a
67
+ * variable lookup (unlike Twig's hash literal, where an unquoted key reads
68
+ * as a variable reference) — but this adapter quotes unconditionally anyway
69
+ * for one uniform rule across every adapter in the family (mirrors
70
+ * `twigHashKey`/`jinjaHashKey`).
71
+ */
72
+ export function bladeHashKey(name: string): string {
73
+ return `'${escapeBladeSingleQuoted(name)}'`
74
+ }
75
+
76
+ /**
77
+ * Blade-reserved bare words: names that collide with the Blade/illuminate
78
+ * RENDER-TIME PHP SCOPE (not Blade template syntax — see the file header,
79
+ * divergence 4). Frozen by the adapter design doc; mirrored EXACTLY by
80
+ * `php/src/naming.php`'s `BLADE_RESERVED_WORDS` (parity test on both sides):
81
+ *
82
+ * - `bf` — the runtime binding every `$bf->method(...)` call resolves
83
+ * against; a same-named prop would silently overwrite it.
84
+ * - `this` — PHP forbids assigning to `$this` outside object context.
85
+ * - `__env` — illuminate/view's `Factory` binds `$__env` in every
86
+ * compiled view's scope.
87
+ * - `__data` — `PhpEngine`/`View`'s internal name for the variable bag
88
+ * before `extract()`.
89
+ * - `__path` — `PhpEngine::evaluatePath()`'s compiled-file-path variable.
90
+ * - `app` — conventionally the container in many Blade/illuminate
91
+ * integrations' base view scope (defensive; this adapter's
92
+ * own standalone `Factory` does not bind it).
93
+ * - `loop` — Blade's `@foreach` directive injects `$loop` (iteration
94
+ * metadata) into the loop body scope.
95
+ */
96
+ const RESERVED_WORDS = new Set(['bf', 'this', '__env', '__data', '__path', 'app', 'loop'])
97
+
98
+ /**
99
+ * Mangle a JS identifier (prop name, signal getter, loop param, …) into a
100
+ * Blade-safe NAME: reserved words get a trailing `_` suffix, everything else
101
+ * passes through unchanged. This is the BARE (no `$`) form — used for a PHP
102
+ * array KEY a prop is threaded through as (`naming.php`'s `blade_ident`,
103
+ * `BladeBackend::render_named`'s per-prop mangling). Every place the adapter
104
+ * emits an actual Blade VARIABLE REFERENCE uses `bladeVar` (below), which
105
+ * wraps this with the `$` sigil Twig's `twigIdent` never needed.
106
+ */
107
+ export function bladeIdent(name: string): string {
108
+ return RESERVED_WORDS.has(name) ? `${name}_` : name
109
+ }
110
+
111
+ /**
112
+ * Mangle a JS identifier into a Blade VARIABLE REFERENCE (`$name` or
113
+ * `$name_` when reserved) — the sigil-bearing counterpart of `bladeIdent`,
114
+ * used at every point the adapter emits a bare variable read OR a
115
+ * `@php($NAME = ...)` assignment target (mirrors every `twigIdent(...)`
116
+ * call site in the Twig port, since Twig had no sigil to add).
117
+ */
118
+ export function bladeVar(name: string): string {
119
+ return `$${bladeIdent(name)}`
120
+ }
121
+
122
+ /**
123
+ * Member access lowering: `a.b` → `data_get($a, 'b')`. Uniform, null-safe
124
+ * over `stdClass` (JSON-decoded objects), PHP assoc arrays (runtime helper
125
+ * outputs), and `null` — verified empirically:
126
+ *
127
+ * php -r 'require "vendor/autoload.php";
128
+ * $o = json_decode("{\"a\":{\"b\":1}}");
129
+ * var_dump(data_get($o, "a.b")); // int(1)
130
+ * var_dump(data_get($o, "missing.x")); // NULL (no warning)
131
+ * var_dump(data_get(null, "a.b")); // NULL (no warning)'
132
+ *
133
+ * `data_get` treats a key with no `.` as ONE top-level segment (not a
134
+ * multi-hop path), so a literal property name that itself happens to
135
+ * contain a `.` character would be misread as a nested path — accepted as a
136
+ * known, narrow limitation (JS field/prop names essentially never contain a
137
+ * literal dot; no adapter-conformance fixture does).
138
+ */
139
+ export function bladeMemberAccess(objectExpr: string, property: string): string {
140
+ return `data_get(${objectExpr}, '${escapeBladeSingleQuoted(property)}')`
141
+ }
142
+
143
+ /**
144
+ * Index access lowering: `a[i]` → `data_get($a, $i)`. `data_get` accepts a
145
+ * non-string (int, or a PHP expression evaluating to int/string) segment
146
+ * directly — verified empirically: `data_get(['a','b','c'], $i)` with
147
+ * `$i = 1` → `'b'`. Same null-safety as `bladeMemberAccess`.
148
+ */
149
+ export function bladeIndexAccess(objectExpr: string, indexExpr: string): string {
150
+ return `data_get(${objectExpr}, ${indexExpr})`
151
+ }
152
+
153
+ /**
154
+ * Build a Blade/PHP accessor expression that walks a `.map()` destructure
155
+ * binding's structured `segments` path (#2087 Phase A, `LoopBindingPathSegment`)
156
+ * off `base` — the per-iteration loop var (`$__bf_item`) for a fixed binding
157
+ * or a top-level rest binding, or an already-built PARENT accessor for a
158
+ * nested rest binding (`segments` there is the prefix up to, not including,
159
+ * the rest token).
160
+ *
161
+ * UNLIKE `twigLoopBindingAccessor` (which branches on bracket-subscript vs.
162
+ * dot-access vs. the `attribute()` builtin depending on whether a `field`
163
+ * segment's key is identifier-safe), every segment here — `index` OR `field`,
164
+ * ident-safe or not — routes through the SAME uniform `data_get` accessor
165
+ * (see the file header, divergence 3): there is no raw-PHP polymorphic
166
+ * operator to special-case around in the first place, so there is nothing to
167
+ * branch on. This is strictly SIMPLER than the Twig port.
168
+ */
169
+ export function bladeLoopBindingAccessor(
170
+ base: string,
171
+ segments: readonly LoopBindingPathSegment[],
172
+ ): string {
173
+ let acc = base
174
+ for (const seg of segments) {
175
+ acc = seg.kind === 'index'
176
+ ? `data_get(${acc}, ${seg.index})`
177
+ : bladeMemberAccess(acc, seg.key)
178
+ }
179
+ return acc
180
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Compile-time constant tables for the Blade template adapter.
3
+ *
4
+ * Ported from `packages/adapter-jinja/src/adapter/lib/constants.ts`.
5
+ */
6
+
7
+ import type { PrimitiveSpec } from './types.ts'
8
+
9
+ /**
10
+ * Single source of truth for the Blade adapter's template-primitive surface.
11
+ * Each entry pairs the expected arity with the emit function.
12
+ *
13
+ * The emit fn returns a Blade/PHP expression (no surrounding `{{ }}`)
14
+ * suitable for embedding inside an interpolation — `$bf->json($val)`,
15
+ * `$bf->floor($val)`, etc. Same primitive names and keys as the Jinja/Twig
16
+ * adapters; every `bf.xxx(...)` call becomes `$bf->xxx(...)` (mapping table).
17
+ */
18
+ export const BLADE_TEMPLATE_PRIMITIVES: Record<string, PrimitiveSpec> = {
19
+ 'JSON.stringify': { arity: 1, emit: (args) => `$bf->json(${args[0]})` },
20
+ 'String': { arity: 1, emit: (args) => `$bf->string(${args[0]})` },
21
+ 'Number': { arity: 1, emit: (args) => `$bf->number(${args[0]})` },
22
+ 'Math.floor': { arity: 1, emit: (args) => `$bf->floor(${args[0]})` },
23
+ 'Math.ceil': { arity: 1, emit: (args) => `$bf->ceil(${args[0]})` },
24
+ 'Math.round': { arity: 1, emit: (args) => `$bf->round(${args[0]})` },
25
+ }
26
+
27
+ /**
28
+ * Module-scope `templatePrimitives` map derived once from the spec record.
29
+ */
30
+ export const BLADE_PRIMITIVE_EMIT_MAP: Record<string, (args: string[]) => string> =
31
+ Object.fromEntries(
32
+ Object.entries(BLADE_TEMPLATE_PRIMITIVES).map(([k, v]) => [k, v.emit])
33
+ )
@@ -0,0 +1,83 @@
1
+ /**
2
+ * IR traversal helpers for the Blade template adapter.
3
+ *
4
+ * Ported from `packages/adapter-twig/src/adapter/lib/ir-scope.ts`.
5
+ * `resolveJsxChildrenProp` and `collectRootScopeNodes` are byte-identical
6
+ * (adapter-agnostic IR walks).
7
+ *
8
+ * `extractTopLevelIdentifiers` is SIMPLER than the Twig port's. Twig
9
+ * identifiers have no sigil, so that port had to strip quoted strings first,
10
+ * exclude dotted property/method names, and drop a closed set of Twig-
11
+ * grammar keywords the adapter's own codegen could emit (`is`, `defined`,
12
+ * `and`, `not`, `null`, `true`, `false`, `bf`) to avoid false-matching one of
13
+ * those as a genuine context-var reference. Blade variables carry PHP's `$`
14
+ * sigil — the SAME property that made Kolon's own `\$([A-Za-z_]\w*)` scan
15
+ * trivially safe (see the Twig port's file header, which calls this out
16
+ * explicitly as the thing Twig's sigil-less grammar lacked). So this port
17
+ * needs no quote-stripping, no dotted-name exclusion (member/index access
18
+ * here is `data_get(...)`, a function call, not a `.`/`[]` postfix that
19
+ * could attach to a preceding bare word), and no keyword-exclusion set at
20
+ * all — the ONLY non-context-var `$name` this adapter's own codegen ever
21
+ * emits is `$bf` (the runtime handle), excluded explicitly below.
22
+ */
23
+
24
+ import type { IRNode, IRProp, IRIfStatement, IRFragment } from '@barefootjs/jsx'
25
+
26
+ /**
27
+ * Extract the set of "top-level identifier" tokens (bare `$name` references,
28
+ * `$bf` excluded) from a rendered Blade expression. `memo/seed.ts` uses this
29
+ * to detect a constant lowering (no real variable reference at all) that
30
+ * should keep the static ssr-defaults seed instead of an in-template
31
+ * `@php($x = ...)`; scope AVAILABILITY itself is the shared
32
+ * `computeSsrSeedPlan`'s job (packages/jsx/src/ssr-seed-plan.ts), not this
33
+ * module's.
34
+ */
35
+ export function extractTopLevelIdentifiers(bladeExpr: string): string[] {
36
+ // Strip single-quoted string literals (this adapter only ever emits
37
+ // single-quoted string literals, backslash-escaped) so a literal `$name`-
38
+ // shaped substring inside one can't leak into the scan.
39
+ const stripped = bladeExpr.replace(/'(?:\\.|[^'\\])*'/g, ' ')
40
+ const out: string[] = []
41
+ for (const m of stripped.matchAll(/\$([A-Za-z_]\w*)/g)) {
42
+ if (m[1] !== 'bf') out.push(m[1])
43
+ }
44
+ return out
45
+ }
46
+
47
+ /**
48
+ * Find the `children` prop's `jsx-children` payload. Narrowed via the
49
+ * AttrValue `kind` discriminator so adapter code stays type-safe if the IR
50
+ * shape evolves.
51
+ */
52
+ export function resolveJsxChildrenProp(props: readonly IRProp[]): IRNode[] {
53
+ const prop = props.find(p => p.name === 'children')
54
+ if (!prop) return []
55
+ if (prop.value.kind !== 'jsx-children') return []
56
+ return prop.value.children
57
+ }
58
+
59
+ /**
60
+ * Collect the component's root scope element node(s) — the elements that
61
+ * become the rendered root and so carry `data-key` for a keyed loop item. A
62
+ * plain element root is itself; an `if-statement` (early-return) root
63
+ * contributes the top element of each branch, since exactly one renders at
64
+ * runtime. (#1297)
65
+ */
66
+ export function collectRootScopeNodes(node: IRNode): Set<IRNode> {
67
+ const out = new Set<IRNode>()
68
+ const visit = (n: IRNode | null): void => {
69
+ if (!n) return
70
+ if (n.type === 'element') { out.add(n); return }
71
+ if (n.type === 'if-statement') {
72
+ const s = n as IRIfStatement
73
+ visit(s.consequent)
74
+ visit(s.alternate)
75
+ return
76
+ }
77
+ if (n.type === 'fragment') {
78
+ for (const c of (n as IRFragment).children) visit(c)
79
+ }
80
+ }
81
+ visit(node)
82
+ return out
83
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Shared type aliases for the Blade template adapter.
3
+ *
4
+ * Ported from `packages/adapter-jinja/src/adapter/lib/types.ts`: pure type
5
+ * declarations — no runtime behaviour — so the extracted emit modules and
6
+ * the main adapter share one definition rather than re-declaring the
7
+ * render context / options shape.
8
+ */
9
+
10
+ /** A template-primitive spec: expected call arity + the emit fn. */
11
+ export interface PrimitiveSpec {
12
+ arity: number
13
+ emit: (args: string[]) => string
14
+ }
15
+
16
+ /**
17
+ * Blade adapter's IRNode render context. Like the Jinja adapter, Blade's
18
+ * lowering doesn't consume any render-position flags, so the Ctx is empty.
19
+ * Kept as a named alias so future flags can extend it without changing the
20
+ * `IRNodeEmitter` interface.
21
+ */
22
+ export type BladeRenderCtx = Record<string, never>
23
+
24
+ export interface BladeAdapterOptions {
25
+ /** Base path for client JS files (default: '/static/components/') */
26
+ clientJsBasePath?: string
27
+
28
+ /** Path to barefoot.js runtime (default: '/static/components/barefoot.js') */
29
+ barefootJsPath?: string
30
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * In-template memo / context seeding for the Blade template adapter.
3
+ *
4
+ * Ported from `packages/adapter-twig/src/adapter/memo/seed.ts`. Free
5
+ * functions taking a `BladeMemoContext` (built by the adapter's `memoCtx`
6
+ * getter) so the cluster depends only on the recursive expression entry, not
7
+ * the whole adapter class. These emit the `@php($x = ...)` statements that
8
+ * let the body's bare `$x` resolve to a derived signal/memo value or an
9
+ * active context value at SSR time.
10
+ *
11
+ * One deliberate behavioural IMPROVEMENT over the Kolon port, carried
12
+ * through from the Twig/Jinja ports unchanged: Kolon's `my` declares a NEW
13
+ * lexical, so `: my $x = … $x …;` reads the not-yet-assigned lexical on its
14
+ * own right-hand side — broken. Blade's `@php($x = ...)` compiles to a
15
+ * plain PHP assignment statement — PHP has function-level (not block-level)
16
+ * scope, and the view-rendering method already `extract()`ed the incoming
17
+ * template vars into real PHP variables before this statement runs, so
18
+ * `@php($x = $x + 1)` simply reads-then-reassigns the SAME `$x` — no
19
+ * shadowing hazard (verified empirically:
20
+ * `$factory->make('t', ['x' => 5])->render()` over a template whose body is
21
+ * `@php($x = $x + 1)\n{{ $x }}` → `"6"`, not an error or a stale `5` — same
22
+ * behaviour Twig/Jinja exhibit). This adapter therefore seeds a same-name
23
+ * signal/memo too, which is strictly more correct — the seed then
24
+ * re-derives from the already-bound prop instead of leaving the render var
25
+ * on its static (possibly per-callsite-wrong) default. No self-ref guard is
26
+ * ported for this reason (same as the Twig port's divergence 8).
27
+ */
28
+
29
+ import {
30
+ type ComponentIR,
31
+ type ContextConsumer,
32
+ collectContextConsumers,
33
+ computeSsrSeedPlan,
34
+ materializeGetterCalls,
35
+ } from '@barefootjs/jsx'
36
+
37
+ import type { BladeMemoContext } from '../emit-context.ts'
38
+ import { extractTopLevelIdentifiers } from '../lib/ir-scope.ts'
39
+ import { bladeVar, escapeBladeSingleQuoted } from '../lib/blade-naming.ts'
40
+
41
+ /** Blade/PHP literal for a context-consumer's `createContext` default. */
42
+ export function contextDefaultBlade(c: ContextConsumer): string {
43
+ const d = c.defaultValue
44
+ if (d === null || d === undefined) return 'null'
45
+ if (typeof d === 'string') return `'${escapeBladeSingleQuoted(d)}'`
46
+ if (typeof d === 'boolean') return d ? 'true' : 'false'
47
+ return String(d)
48
+ }
49
+
50
+ /**
51
+ * Emit one `@php($local = $bf->use_context(...))` statement per context
52
+ * consumer so the body's bare `$local` resolves to the active provider
53
+ * value (or the `createContext` default). (#1297)
54
+ */
55
+ export function generateContextConsumerSeed(ir: ComponentIR): string {
56
+ const consumers = collectContextConsumers(ir.metadata)
57
+ if (consumers.length === 0) return ''
58
+ return (
59
+ consumers
60
+ .map(
61
+ c =>
62
+ `@php(${bladeVar(c.localName)} = $bf->use_context('${c.contextName}', ${contextDefaultBlade(c)}))`,
63
+ )
64
+ .join('\n') + '\n'
65
+ )
66
+ }
67
+
68
+ /**
69
+ * Emit `@php($name = <blade>)` statements for every `derived` step of the
70
+ * backend-neutral SSR seed plan — the scope/availability/ordering analysis
71
+ * lives in `computeSsrSeedPlan` (packages/jsx/src/ssr-seed-plan.ts); this
72
+ * only lowers each step's expression to Blade and applies the
73
+ * backend-specific emit guards: skip an empty lowering, and skip a lowering
74
+ * that references no top-level identifier at all (a constant init/body —
75
+ * e.g. a `derived` step with empty `frees` — keeps the existing static
76
+ * ssr-defaults seed instead). Unlike the Kolon port there is no self-ref
77
+ * guard — see the file header. `env-reader` and `opaque` steps emit nothing
78
+ * (the runtime supplies the reader, or the adapter's ssr-defaults path
79
+ * already covers it). (#1297, #2075)
80
+ *
81
+ * `convertExpressionToBlade` can still fail DEEPER than the plan's
82
+ * structural `isSupported` pre-check — e.g. a `.filter(predicate)` whose
83
+ * predicate isn't evaluator-serializable has NO lambda fallback on this
84
+ * adapter (`expr/emitters.ts`'s file header, divergence 9: unlike Kolon/Perl,
85
+ * Blade has no lambda-expression form), so the lowering records a HARD
86
+ * BF101 as a side effect rather than degrading. That's correct for every
87
+ * OTHER `convertExpressionToBlade` call site (which commit to using the
88
+ * lowered text), but wrong here: this is a SPECULATIVE "recompute the memo
89
+ * in-template, else keep the static ssrDefault seed" attempt, so a deeper
90
+ * refusal must degrade silently, not fail the whole component compile.
91
+ * Snapshot the diagnostic list and roll back any errors appended during each
92
+ * step's attempt before moving on.
93
+ *
94
+ * A second, related divergence opens up: a predicate referencing a SIBLING
95
+ * getter (`props.items.filter((p) => !tag() || …)`, `tag` a sibling memo)
96
+ * contains a zero-arg CALL node (`tag()`), which the evaluator's pure-
97
+ * expression surface refuses (`toEvalNode`'s `call` arm only allows a
98
+ * builtin callee, e.g. `Math.floor`) — with no lambda fallback to fall back
99
+ * to, the whole predicate would refuse. Kolon/Perl never hit this: their
100
+ * lambda form closes over the sibling's ALREADY-SEEDED lexical directly,
101
+ * without going through the evaluator at all. Go hits the identical gap (it
102
+ * also has no closures at SSR-constructor time) and fixes it the same way
103
+ * its `memo/memo-compute.ts` (`matchFilterArmMemo`) does:
104
+ * `materializeGetterCalls` rewrites a getter call into a bare identifier
105
+ * BEFORE serialization, so the evaluator captures it as a free-var read from
106
+ * `base_env` instead of an unsupported call node — and that free var then
107
+ * resolves against the sibling's own `@php($x = ...)` line, which (being an
108
+ * EARLIER step per the plan's ordering guarantee) is always already bound.
109
+ * `env-reader` names are excluded from the materializable set — the runtime
110
+ * supplies those via the per-request reader, not a template-var lexical, so
111
+ * a call to one (`searchParams()`) must stay a real call, not a bare var.
112
+ */
113
+ export function generateDerivedMemoSeed(ctx: BladeMemoContext, ir: ComponentIR): string {
114
+ // Package G attached this to metadata at compile time; the `??` fallback
115
+ // only covers hand-built metadata in older tests that predate the attached
116
+ // plan — same shared function, so there's no divergence from the compiler.
117
+ const plan = ir.metadata.ssrSeedPlan ?? computeSsrSeedPlan(ir.metadata)
118
+ const knownGetterNames = new Set<string>(
119
+ plan.steps.filter(s => s.kind !== 'env-reader').map(s => s.name),
120
+ )
121
+ const lines: string[] = []
122
+ for (const step of plan.steps) {
123
+ if (step.kind !== 'derived') continue
124
+ const materialized = materializeGetterCalls(step.parsed, knownGetterNames)
125
+ const errorsBefore = ctx.errors.length
126
+ const blade = ctx.convertExpressionToBlade('', materialized)
127
+ if (ctx.errors.length > errorsBefore) {
128
+ ctx.errors.length = errorsBefore
129
+ continue
130
+ }
131
+ if (blade === '' || extractTopLevelIdentifiers(blade).length === 0) continue
132
+ lines.push(`@php(${bladeVar(step.name)} = ${blade})`)
133
+ }
134
+ return lines.length > 0 ? lines.join('\n') + '\n' : ''
135
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Prop classification for the Blade template adapter.
3
+ *
4
+ * Ported from `packages/adapter-jinja/src/adapter/props/prop-classes.ts`.
5
+ * Pure functions over `ir.metadata` that derive the per-compile prop/name
6
+ * sets the adapter consults during lowering. No adapter instance state.
7
+ */
8
+
9
+ import type { ComponentIR } from '@barefootjs/jsx'
10
+ import { isStringTypeInfo, isBareStringLiteral } from '../value/parsed-literal.ts'
11
+
12
+ /**
13
+ * Props whose declared TS type is boolean — a bare binding of one
14
+ * (`data-active={props.isActive}`) must stringify as JS `String(boolean)`
15
+ * ("true"/"false"), not PHP's `(string) bool` ("1"/"") (#1897, pagination's
16
+ * data-active).
17
+ */
18
+ export function collectBooleanTypedProps(ir: ComponentIR): Set<string> {
19
+ return new Set(
20
+ ir.metadata.propsParams
21
+ .filter(prop => prop.type?.primitive === 'boolean' || prop.type?.raw === 'boolean')
22
+ .map(prop => prop.name),
23
+ )
24
+ }
25
+
26
+ /**
27
+ * Bare references to optional, no-default, non-primitive props (e.g.
28
+ * textarea's `rows`) are `null` when omitted → guarded with
29
+ * `is defined and is not null` in `emitExpression`. See the
30
+ * `nullableOptionalProps` field docstring in `blade-adapter.ts`.
31
+ */
32
+ export function collectNullableOptionalProps(ir: ComponentIR): Set<string> {
33
+ return new Set(
34
+ ir.metadata.propsParams
35
+ .filter(
36
+ p =>
37
+ p.defaultValue === undefined &&
38
+ !p.isRest &&
39
+ p.type?.kind !== 'primitive',
40
+ )
41
+ .map(p => p.name),
42
+ )
43
+ }
44
+
45
+ /**
46
+ * String-typed signals and props. A signal is string-typed when its inferred
47
+ * type is `string` (or, defensively, when its initial value is a bare string
48
+ * literal); a prop when its annotated type is `string`. In the Mojo adapter
49
+ * this drives `eq`/`ne` selection for string equality; the Blade emitters
50
+ * don't consume the distinction — `===`/`!==` ALWAYS route through
51
+ * `bf.eq`/`bf.neq` regardless of operand type (see
52
+ * `expr/emitters.ts`'s file header, divergence 4) — so this set is carried
53
+ * only for parity with the Perl-family adapters.
54
+ */
55
+ export function collectStringValueNames(ir: ComponentIR): Set<string> {
56
+ const names = new Set<string>()
57
+ for (const s of ir.metadata.signals) {
58
+ if (isStringTypeInfo(s.type) || isBareStringLiteral(s.initialValue)) {
59
+ names.add(s.getter)
60
+ }
61
+ }
62
+ for (const p of ir.metadata.propsParams) {
63
+ if (isStringTypeInfo(p.type)) names.add(p.name)
64
+ }
65
+ return names
66
+ }