@barefootjs/erb 0.17.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/dist/adapter/analysis/component-tree.d.ts +24 -0
- package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
- package/dist/adapter/boolean-result.d.ts +66 -0
- package/dist/adapter/boolean-result.d.ts.map +1 -0
- package/dist/adapter/emit-context.d.ts +107 -0
- package/dist/adapter/emit-context.d.ts.map +1 -0
- package/dist/adapter/erb-adapter.d.ts +374 -0
- package/dist/adapter/erb-adapter.d.ts.map +1 -0
- package/dist/adapter/expr/array-method.d.ts +87 -0
- package/dist/adapter/expr/array-method.d.ts.map +1 -0
- package/dist/adapter/expr/emitters.d.ts +102 -0
- package/dist/adapter/expr/emitters.d.ts.map +1 -0
- package/dist/adapter/expr/operand.d.ts +34 -0
- package/dist/adapter/expr/operand.d.ts.map +1 -0
- package/dist/adapter/index.d.ts +6 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/index.js +189154 -0
- package/dist/adapter/lib/constants.d.ts +25 -0
- package/dist/adapter/lib/constants.d.ts.map +1 -0
- package/dist/adapter/lib/ir-scope.d.ts +27 -0
- package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
- package/dist/adapter/lib/ruby-naming.d.ts +65 -0
- package/dist/adapter/lib/ruby-naming.d.ts.map +1 -0
- package/dist/adapter/lib/types.d.ts +28 -0
- package/dist/adapter/lib/types.d.ts.map +1 -0
- package/dist/adapter/memo/seed.d.ts +35 -0
- package/dist/adapter/memo/seed.d.ts.map +1 -0
- package/dist/adapter/props/prop-classes.d.ts +50 -0
- package/dist/adapter/props/prop-classes.d.ts.map +1 -0
- package/dist/adapter/spread/spread-codegen.d.ts +63 -0
- package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
- package/dist/adapter/value/parsed-literal.d.ts +21 -0
- package/dist/adapter/value/parsed-literal.d.ts.map +1 -0
- package/dist/build.d.ts +28 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +189174 -0
- package/dist/conformance-pins.d.ts +13 -0
- package/dist/conformance-pins.d.ts.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +189172 -0
- package/lib/barefoot_js/backend/erb.rb +123 -0
- package/lib/barefoot_js/dev_reload.rb +159 -0
- package/lib/barefoot_js/evaluator.rb +714 -0
- package/lib/barefoot_js/search_params.rb +63 -0
- package/lib/barefoot_js.rb +1155 -0
- package/package.json +67 -0
- package/src/__tests__/erb-adapter.test.ts +298 -0
- package/src/adapter/analysis/component-tree.ts +122 -0
- package/src/adapter/boolean-result.ts +157 -0
- package/src/adapter/emit-context.ts +119 -0
- package/src/adapter/erb-adapter.ts +1768 -0
- package/src/adapter/expr/array-method.ts +424 -0
- package/src/adapter/expr/emitters.ts +629 -0
- package/src/adapter/expr/operand.ts +55 -0
- package/src/adapter/index.ts +6 -0
- package/src/adapter/lib/constants.ts +37 -0
- package/src/adapter/lib/ir-scope.ts +51 -0
- package/src/adapter/lib/ruby-naming.ts +127 -0
- package/src/adapter/lib/types.ts +31 -0
- package/src/adapter/memo/seed.ts +75 -0
- package/src/adapter/props/prop-classes.ts +89 -0
- package/src/adapter/spread/spread-codegen.ts +176 -0
- package/src/adapter/value/parsed-literal.ts +29 -0
- package/src/build.ts +37 -0
- package/src/conformance-pins.ts +100 -0
- package/src/index.ts +9 -0
- package/src/test-render.ts +668 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile-time constant tables for the ERB template adapter.
|
|
3
|
+
*
|
|
4
|
+
* Ported from the Mojolicious adapter's `lib/constants.ts`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { PrimitiveSpec } from './types.ts'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Single source of truth for the ERB adapter's template-primitive
|
|
11
|
+
* surface. Each entry pairs the expected arity with the emit function.
|
|
12
|
+
* Adding / removing a primitive is a one-line change.
|
|
13
|
+
*
|
|
14
|
+
* The emit fn returns a Ruby expression (no surrounding `<%= %>`)
|
|
15
|
+
* suitable for embedding inside the ERB template action —
|
|
16
|
+
* `bf.json(val)`, `bf.floor(val)`, etc. Args arrive already
|
|
17
|
+
* Ruby-rendered via `convertExpressionToRuby` recursion, so a caller
|
|
18
|
+
* passing `props.config` reaches the emit fn as `v[:config]`.
|
|
19
|
+
*/
|
|
20
|
+
export const ERB_TEMPLATE_PRIMITIVES: Record<string, PrimitiveSpec> = {
|
|
21
|
+
'JSON.stringify': { arity: 1, emit: (args) => `bf.json(${args[0]})` },
|
|
22
|
+
'String': { arity: 1, emit: (args) => `bf.string(${args[0]})` },
|
|
23
|
+
'Number': { arity: 1, emit: (args) => `bf.number(${args[0]})` },
|
|
24
|
+
'Math.floor': { arity: 1, emit: (args) => `bf.floor(${args[0]})` },
|
|
25
|
+
'Math.ceil': { arity: 1, emit: (args) => `bf.ceil(${args[0]})` },
|
|
26
|
+
'Math.round': { arity: 1, emit: (args) => `bf.round(${args[0]})` },
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Module-scope `templatePrimitives` map derived once from the spec
|
|
31
|
+
* record. Per-instance derivation would re-build the same Map on
|
|
32
|
+
* every `new ErbAdapter()` call.
|
|
33
|
+
*/
|
|
34
|
+
export const ERB_PRIMITIVE_EMIT_MAP: Record<string, (args: string[]) => string> =
|
|
35
|
+
Object.fromEntries(
|
|
36
|
+
Object.entries(ERB_TEMPLATE_PRIMITIVES).map(([k, v]) => [k, v.emit])
|
|
37
|
+
)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IR traversal helpers for the ERB template adapter.
|
|
3
|
+
*
|
|
4
|
+
* Ported from the Mojolicious adapter's `lib/ir-scope.ts` (issue #2018
|
|
5
|
+
* track D lineage). Pure functions over the IR tree — no adapter instance
|
|
6
|
+
* state.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { IRNode, IRProp, IRIfStatement, IRFragment } from '@barefootjs/jsx'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Find the `children` prop's `jsx-children` payload (#1326). Narrowed
|
|
13
|
+
* via the AttrValue `kind` discriminator so adapter code stays type-
|
|
14
|
+
* safe if the IR shape evolves — adding a new AttrValue variant or
|
|
15
|
+
* renaming `children` to `jsxChildren` becomes a TS compile error
|
|
16
|
+
* here instead of silently dropping the children at runtime.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveJsxChildrenProp(props: readonly IRProp[]): IRNode[] {
|
|
19
|
+
const prop = props.find(p => p.name === 'children')
|
|
20
|
+
if (!prop) return []
|
|
21
|
+
if (prop.value.kind !== 'jsx-children') return []
|
|
22
|
+
return prop.value.children
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Collect the component's root scope element node(s) — the elements that
|
|
27
|
+
* become the rendered root and so carry `data-key` for a keyed loop item. A
|
|
28
|
+
* plain element root is itself; an `if-statement` (early-return) root
|
|
29
|
+
* contributes the top element of each branch (`consequent` + the `alternate`
|
|
30
|
+
* chain), since exactly one branch renders at runtime. Non-element branch
|
|
31
|
+
* tops (fragments / nested shapes) are walked one level so an
|
|
32
|
+
* `if (…) return <A/>` still resolves to `<A>`. (#1297)
|
|
33
|
+
*/
|
|
34
|
+
export function collectRootScopeNodes(node: IRNode): Set<IRNode> {
|
|
35
|
+
const out = new Set<IRNode>()
|
|
36
|
+
const visit = (n: IRNode | null): void => {
|
|
37
|
+
if (!n) return
|
|
38
|
+
if (n.type === 'element') { out.add(n); return }
|
|
39
|
+
if (n.type === 'if-statement') {
|
|
40
|
+
const s = n as IRIfStatement
|
|
41
|
+
visit(s.consequent)
|
|
42
|
+
visit(s.alternate)
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
if (n.type === 'fragment') {
|
|
46
|
+
for (const c of (n as IRFragment).children) visit(c)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
visit(node)
|
|
50
|
+
return out
|
|
51
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ruby identifier / literal / hash-key conventions for the ERB adapter.
|
|
3
|
+
*
|
|
4
|
+
* Pure helpers, ported from (and extending) the Mojolicious adapter's
|
|
5
|
+
* `lib/perl-naming.ts` for Ruby's syntax. None read adapter instance
|
|
6
|
+
* state, so they live at module scope as the single source of truth for
|
|
7
|
+
* Ruby-identifier quoting, literal escaping, and marker-id encoding.
|
|
8
|
+
*
|
|
9
|
+
* ## Variable model
|
|
10
|
+
*
|
|
11
|
+
* Templates receive exactly two locals: `bf` (runtime) and `v` (vars Hash,
|
|
12
|
+
* symbol keys). Every prop / signal / memo / module-constant reference
|
|
13
|
+
* lowers to `v[:name]` — never a bare Ruby local — which sidesteps Ruby
|
|
14
|
+
* identifier-validity and reserved-word issues for that whole class of
|
|
15
|
+
* names (a prop literally named `class` or `Foo` is a non-issue: it's a
|
|
16
|
+
* *symbol* key, not a variable reference).
|
|
17
|
+
*
|
|
18
|
+
* The ONE place a bare Ruby local is still needed is a loop/block
|
|
19
|
+
* parameter (`todos().map(todo => ...)` → `|todo|`) — `rubyLocal` is the
|
|
20
|
+
* single naming rule for that case, matching the ERB adapter's binding
|
|
21
|
+
* architecture doc.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Ruby's reserved words — cannot be used as a local variable / block
|
|
26
|
+
* parameter name (the parser reads them as keywords, not identifiers).
|
|
27
|
+
* `self`, `nil`, `true`, `false` are technically pseudo-variables/keywords
|
|
28
|
+
* with special meaning even in expression position, so they're included
|
|
29
|
+
* too — using one of these as a block param name is either a SyntaxError
|
|
30
|
+
* or silently shadows a builtin literal, either of which we want to avoid
|
|
31
|
+
* for the same "load-bearing loop var" reason the Perl side avoids
|
|
32
|
+
* `my $if = ...`.
|
|
33
|
+
*/
|
|
34
|
+
const RUBY_KEYWORDS: ReadonlySet<string> = new Set([
|
|
35
|
+
'__ENCODING__', '__LINE__', '__FILE__',
|
|
36
|
+
'BEGIN', 'END',
|
|
37
|
+
'alias', 'and', 'begin', 'break', 'case', 'class', 'def', 'defined?',
|
|
38
|
+
'do', 'else', 'elsif', 'end', 'ensure', 'false', 'for', 'if', 'in',
|
|
39
|
+
'module', 'next', 'nil', 'not', 'or', 'redo', 'rescue', 'retry',
|
|
40
|
+
'return', 'self', 'super', 'then', 'true', 'undef', 'unless', 'until',
|
|
41
|
+
'when', 'while', 'yield',
|
|
42
|
+
// Not Ruby keywords, but the two RESERVED locals every compiled template
|
|
43
|
+
// receives (the binding architecture contract: `bf` the runtime context,
|
|
44
|
+
// `v` the vars Hash). A loop/block param named `v` or `bf` (e.g.
|
|
45
|
+
// `items.values().map(v => ...)`, whose synthesized `.entries()` value
|
|
46
|
+
// binding is literally `v`) would otherwise shadow the vars Hash inside
|
|
47
|
+
// the loop body, silently breaking every subsequent `v[:name]` read for
|
|
48
|
+
// the rest of that scope. Route through the same collision-suffix path
|
|
49
|
+
// as a real keyword.
|
|
50
|
+
'bf', 'v',
|
|
51
|
+
])
|
|
52
|
+
|
|
53
|
+
/** A syntactically valid Ruby local-variable / block-parameter identifier
|
|
54
|
+
* (must start lowercase-letter-or-underscore; a leading uppercase letter
|
|
55
|
+
* parses as a constant reference, not a variable). */
|
|
56
|
+
const VALID_RUBY_LOCAL = /^[a-z_][A-Za-z0-9_]*$/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Map a JS loop/block parameter name (`todo`, `index`, `class`, `Item`) to
|
|
60
|
+
* a safe bare Ruby local. Appends a trailing `_` when the name collides
|
|
61
|
+
* with a Ruby keyword; when the name isn't even a syntactically valid
|
|
62
|
+
* local (leading uppercase — parses as a constant — or another invalid
|
|
63
|
+
* leading character), prefixes `_` instead so the result still starts
|
|
64
|
+
* lowercase/underscore. Every loop-var / block-param emission site in the
|
|
65
|
+
* adapter goes through this one helper — no inline mangling.
|
|
66
|
+
*/
|
|
67
|
+
export function rubyLocal(name: string): string {
|
|
68
|
+
if (RUBY_KEYWORDS.has(name)) return `${name}_`
|
|
69
|
+
if (VALID_RUBY_LOCAL.test(name)) return name
|
|
70
|
+
// Invalid leading character (most commonly a JS param starting with an
|
|
71
|
+
// uppercase letter, which Ruby would otherwise parse as a constant
|
|
72
|
+
// reference) — prefix rather than suffix so the fixed name still starts
|
|
73
|
+
// with a valid local-variable leading character.
|
|
74
|
+
return `_${name}`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Escape a string for a Ruby single-quoted literal: backslash first (so
|
|
78
|
+
* it doesn't double-escape the quote we add next), then the quote. */
|
|
79
|
+
export function escapeRubySingleQuoted(s: string): string {
|
|
80
|
+
return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Wrap a raw string value as a Ruby single-quoted literal. */
|
|
84
|
+
export function rubyStringLiteral(s: string): string {
|
|
85
|
+
return `'${escapeRubySingleQuoted(s)}'`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** A syntactically valid bare Ruby hash-key / symbol identifier. */
|
|
89
|
+
const VALID_RUBY_SYMBOL = /^[A-Za-z_][A-Za-z0-9_]*[?!]?$/
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Render `name` as a Ruby Hash literal SYMBOL key in `key: value` position.
|
|
93
|
+
* A JSX attribute / prop name like `data-slot` isn't a valid bare Ruby
|
|
94
|
+
* identifier — Ruby's `"quoted": value` symbol-key syntax accepts an
|
|
95
|
+
* arbitrary string, so a non-identifier name still renders as a valid
|
|
96
|
+
* symbol key (`"data-slot": value` → `{:"data-slot" => value}`).
|
|
97
|
+
* Identifier-safe names (`className`, `size`, `_bf_slot`) pass through
|
|
98
|
+
* unquoted (`size: value`) for readability.
|
|
99
|
+
*/
|
|
100
|
+
export function rubySymbolKey(name: string): string {
|
|
101
|
+
return VALID_RUBY_SYMBOL.test(name) ? `${name}:` : `"${name.replace(/"/g, '\\"')}":`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Render `name` as a Ruby symbol LITERAL (`:name` / `:"data-slot"`) — used
|
|
106
|
+
* for a compile-time-known Hash key read (`item[:field]`), as opposed to
|
|
107
|
+
* `rubySymbolKey`'s `key: value` Hash-literal position.
|
|
108
|
+
*/
|
|
109
|
+
export function rubySymbolLiteral(name: string): string {
|
|
110
|
+
return VALID_RUBY_SYMBOL.test(name) ? `:${name}` : `:"${name.replace(/"/g, '\\"')}"`
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Encode an `IRLoop.markerId` into a Ruby-identifier-safe suffix for the
|
|
115
|
+
* `bf_iter_…` sort hoist local. Collision-free for marker ids that differ
|
|
116
|
+
* in any character — `-` and `_` map to distinct encodings (`_x2d` vs
|
|
117
|
+
* `__`) so `l-0` and `l_0` stay distinct.
|
|
118
|
+
*
|
|
119
|
+
* Today the IR only emits `l<digits>` so the encoding is mostly an
|
|
120
|
+
* identity, but pinning collision-freeness up front avoids a silent
|
|
121
|
+
* variable-shadow bug if a future marker generator widens the alphabet.
|
|
122
|
+
*/
|
|
123
|
+
export function rubyIdentifierFromMarkerId(markerId: string): string {
|
|
124
|
+
return markerId.replace(/[^a-zA-Z0-9]/g, (ch) =>
|
|
125
|
+
ch === '_' ? '__' : `_x${ch.charCodeAt(0).toString(16)}`
|
|
126
|
+
)
|
|
127
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared type aliases for the ERB template adapter.
|
|
3
|
+
*
|
|
4
|
+
* Ported from the Mojolicious adapter's `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 render
|
|
7
|
+
* 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
|
+
* ERB adapter's IRNode render context. The ERB lowering currently doesn't
|
|
18
|
+
* consume any render-position flags (`isRootOfClientComponent` is handled
|
|
19
|
+
* differently here than in Hono/Go), so the Ctx is empty. Kept as a named
|
|
20
|
+
* alias so future flags can extend it without changing the `IRNodeEmitter`
|
|
21
|
+
* interface.
|
|
22
|
+
*/
|
|
23
|
+
export type ErbRenderCtx = Record<string, never>
|
|
24
|
+
|
|
25
|
+
export interface ErbAdapterOptions {
|
|
26
|
+
/** Base path for client JS files (default: '/static/components/') */
|
|
27
|
+
clientJsBasePath?: string
|
|
28
|
+
|
|
29
|
+
/** Path to barefoot.js runtime (default: '/static/components/barefoot.js') */
|
|
30
|
+
barefootJsPath?: string
|
|
31
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-template memo / context seeding for the ERB template adapter.
|
|
3
|
+
*
|
|
4
|
+
* Ported from the Mojolicious adapter's `memo/seed.ts` (issue #2018 track D
|
|
5
|
+
* lineage). Free functions taking an `ErbMemoContext` (built by the
|
|
6
|
+
* adapter's `memoCtx` getter) so the cluster depends only on the recursive
|
|
7
|
+
* expression entry, not the whole adapter class. These emit `<% v[:x] =
|
|
8
|
+
* ...; %>` seed lines that let the template body's `v[:x]` resolve to a
|
|
9
|
+
* derived signal/memo value or an active context value at SSR time. Mirror
|
|
10
|
+
* of the Go adapter's `memo/*`, retargeted to the vars-Hash variable model.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
type ComponentIR,
|
|
15
|
+
type ContextConsumer,
|
|
16
|
+
collectContextConsumers,
|
|
17
|
+
computeSsrSeedPlan,
|
|
18
|
+
} from '@barefootjs/jsx'
|
|
19
|
+
|
|
20
|
+
import type { ErbMemoContext } from '../emit-context.ts'
|
|
21
|
+
import { rubyStringLiteral } from '../lib/ruby-naming.ts'
|
|
22
|
+
|
|
23
|
+
/** Ruby literal for a context-consumer's `createContext` default. */
|
|
24
|
+
export function contextDefaultRuby(c: ContextConsumer): string {
|
|
25
|
+
const d = c.defaultValue
|
|
26
|
+
if (d === null || d === undefined) return 'nil'
|
|
27
|
+
if (typeof d === 'string') return rubyStringLiteral(d)
|
|
28
|
+
if (typeof d === 'boolean') return d ? 'true' : 'false'
|
|
29
|
+
return String(d)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Emit one `<% v[:<local>] = bf.use_context(...) %>` seed line per context
|
|
34
|
+
* consumer so the template body's `v[:<local>]` resolves to the active
|
|
35
|
+
* provider value (or the `createContext` default).
|
|
36
|
+
*/
|
|
37
|
+
export function generateContextConsumerSeed(ir: ComponentIR): string {
|
|
38
|
+
const consumers = collectContextConsumers(ir.metadata)
|
|
39
|
+
if (consumers.length === 0) return ''
|
|
40
|
+
return (
|
|
41
|
+
consumers
|
|
42
|
+
.map(
|
|
43
|
+
c =>
|
|
44
|
+
`<% v[:${c.localName}] = bf.use_context('${c.contextName}', ${contextDefaultRuby(c)}) %>`,
|
|
45
|
+
)
|
|
46
|
+
.join('\n') + '\n'
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Emit `<% v[:<name>] = <ruby> %>` seed lines for every `derived` step of
|
|
52
|
+
* the backend-neutral SSR seed plan — the scope/availability/ordering
|
|
53
|
+
* analysis lives in `computeSsrSeedPlan` (packages/jsx/src/ssr-seed-plan.ts);
|
|
54
|
+
* this only lowers each step's expression to Ruby and applies the two
|
|
55
|
+
* backend-specific emit guards: skip an empty lowering, and skip a lowering
|
|
56
|
+
* that references no `v[:var]` at all (a constant init/body — e.g. a
|
|
57
|
+
* `derived` step with empty `frees` — keeps the existing static ssr-defaults
|
|
58
|
+
* seed instead). `env-reader` and `opaque` steps emit nothing (the runtime
|
|
59
|
+
* supplies the reader, or the adapter's ssr-defaults path already covers
|
|
60
|
+
* it). (#1297, #2075)
|
|
61
|
+
*/
|
|
62
|
+
export function generateDerivedMemoSeed(ctx: ErbMemoContext, ir: ComponentIR): string {
|
|
63
|
+
// Package G attached this to metadata at compile time; the `??` fallback
|
|
64
|
+
// only covers hand-built metadata in older tests that predate the attached
|
|
65
|
+
// plan — same shared function, so there's no divergence from the compiler.
|
|
66
|
+
const plan = ir.metadata.ssrSeedPlan ?? computeSsrSeedPlan(ir.metadata)
|
|
67
|
+
const lines: string[] = []
|
|
68
|
+
for (const step of plan.steps) {
|
|
69
|
+
if (step.kind !== 'derived') continue
|
|
70
|
+
const ruby = ctx.convertExpressionToRuby(step.expr, step.parsed)
|
|
71
|
+
if (ruby === '' || !/v\[:[A-Za-z_]\w*\]/.test(ruby)) continue
|
|
72
|
+
lines.push(`<% v[:${step.name}] = ${ruby} %>`)
|
|
73
|
+
}
|
|
74
|
+
return lines.length > 0 ? lines.join('\n') + '\n' : ''
|
|
75
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prop classification for the ERB template adapter.
|
|
3
|
+
*
|
|
4
|
+
* Ported from the Mojolicious adapter's `props/prop-classes.ts` (issue #2018
|
|
5
|
+
* track D lineage). Pure functions over `ir.metadata` that derive the
|
|
6
|
+
* per-compile prop/name sets the adapter consults during lowering. No
|
|
7
|
+
* adapter instance state.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ComponentIR } from '@barefootjs/jsx'
|
|
11
|
+
import { isStringTypeInfo, isBareStringLiteral } from '../value/parsed-literal.ts'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* SSR-resolvable context-value names: props, signal getters, memos.
|
|
15
|
+
* A `<Ctx.Provider value>` member NOT in this set is a client-only function
|
|
16
|
+
* with no SSR value, lowered to `nil`.
|
|
17
|
+
*/
|
|
18
|
+
export function collectProviderDataNames(ir: ComponentIR): Set<string> {
|
|
19
|
+
return new Set<string>([
|
|
20
|
+
...ir.metadata.propsParams.map(p => p.name),
|
|
21
|
+
...(ir.metadata.signals ?? []).map(s => s.getter),
|
|
22
|
+
...(ir.metadata.memos ?? []).map(m => m.name),
|
|
23
|
+
])
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Props whose declared TS type is boolean — a bare binding of one
|
|
28
|
+
* (`data-active={props.isActive}`) must stringify as JS `String(boolean)`
|
|
29
|
+
* ("true"/"false"), matching the `bf.bool_str` helper's output.
|
|
30
|
+
*/
|
|
31
|
+
export function collectBooleanTypedProps(ir: ComponentIR): Set<string> {
|
|
32
|
+
return new Set(
|
|
33
|
+
ir.metadata.propsParams
|
|
34
|
+
.filter(prop => prop.type?.primitive === 'boolean' || prop.type?.raw === 'boolean')
|
|
35
|
+
.map(prop => prop.name),
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* No-destructure-default props → `nil` when the caller omits them → guard
|
|
41
|
+
* their bare-reference attribute emission with a Ruby nil-check so the
|
|
42
|
+
* attribute drops instead of rendering `attr=""` (Hono-style nullish
|
|
43
|
+
* omission). A prop WITH a destructure default (`value = ''`) is never
|
|
44
|
+
* `nil` in the body and must stay unconditional, so it is excluded. Mirrors
|
|
45
|
+
* the Go adapter's nillable-field guard: there the witness is the resolved
|
|
46
|
+
* `interface{}` field type; here it is the absence of a default. Excludes
|
|
47
|
+
* concrete-primitive types (`string`/`number`/`boolean`) to match the Go
|
|
48
|
+
* adapter's scope, which guards only nillable fields and leaves concrete
|
|
49
|
+
* fields unconditional.
|
|
50
|
+
*/
|
|
51
|
+
export function collectNullableOptionalProps(ir: ComponentIR): Set<string> {
|
|
52
|
+
return new Set(
|
|
53
|
+
ir.metadata.propsParams
|
|
54
|
+
.filter(
|
|
55
|
+
p =>
|
|
56
|
+
p.defaultValue === undefined &&
|
|
57
|
+
!p.isRest &&
|
|
58
|
+
p.type?.kind !== 'primitive',
|
|
59
|
+
)
|
|
60
|
+
.map(p => p.name),
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* String-typed signals and props. A signal is string-typed when its inferred
|
|
66
|
+
* type is `string` (the analyzer infers this from a string-literal initial
|
|
67
|
+
* value) or, defensively, when its initial value is a bare string literal; a
|
|
68
|
+
* prop when its annotated type is `string`.
|
|
69
|
+
*
|
|
70
|
+
* Ruby's `==`/`!=` don't coerce operand types the way Perl's numeric `==`
|
|
71
|
+
* does, so this set does NOT drive equality-operator selection in the ERB
|
|
72
|
+
* adapter (unlike Mojo's `eq`/`ne` split). It still matters for **index
|
|
73
|
+
* access**: `obj[index]` lowers a string-typed `index` to a Hash lookup
|
|
74
|
+
* (`obj[index.to_sym]`, JSON-shaped Ruby hashes use symbol keys) and any
|
|
75
|
+
* other type to an Array lookup (`obj[index]`) — see
|
|
76
|
+
* `expr/operand.ts::isStringTypedOperand`.
|
|
77
|
+
*/
|
|
78
|
+
export function collectStringValueNames(ir: ComponentIR): Set<string> {
|
|
79
|
+
const names = new Set<string>()
|
|
80
|
+
for (const s of ir.metadata.signals) {
|
|
81
|
+
if (isStringTypeInfo(s.type) || isBareStringLiteral(s.initialValue)) {
|
|
82
|
+
names.add(s.getter)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
for (const p of ir.metadata.propsParams) {
|
|
86
|
+
if (isStringTypeInfo(p.type)) names.add(p.name)
|
|
87
|
+
}
|
|
88
|
+
return names
|
|
89
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Object-literal / conditional-spread → Ruby Hash lowering for the ERB
|
|
3
|
+
* template adapter.
|
|
4
|
+
*
|
|
5
|
+
* Ported from the Mojolicious adapter's `spread/spread-codegen.ts` (issue
|
|
6
|
+
* #2018 track D lineage). Free functions taking an `ErbSpreadContext` (built
|
|
7
|
+
* by the adapter's `spreadCtx` getter) so the cluster depends on the narrow
|
|
8
|
+
* seam — the recursive expression entry plus per-compile bookkeeping —
|
|
9
|
+
* rather than the whole adapter class. Mirror of the Go adapter's
|
|
10
|
+
* `spread/spread-codegen.ts`.
|
|
11
|
+
*
|
|
12
|
+
* The conditional-spread / object-literal entries read the IR-carried
|
|
13
|
+
* structured `ParsedExpr` tree instead of re-parsing the source with
|
|
14
|
+
* `ts.createSourceFile`. The condition and scalar values are threaded
|
|
15
|
+
* straight into `ctx.convertExpressionToRuby` as its `preParsed` argument
|
|
16
|
+
* (cf. go-template's `convertExpressionToGo(jsExpr, out?, preParsed?)`), so
|
|
17
|
+
* no stringify→re-parse round-trip occurs. The `ts.factory` rebuild in
|
|
18
|
+
* `recordIndexAccessToRuby` only reconstructs the `IDENT[KEY]` node the
|
|
19
|
+
* shared `parseRecordIndexAccess` parser accepts; no source-text re-parse.
|
|
20
|
+
* `stringifyParsedExpr` is retained solely for the BF101 diagnostic message
|
|
21
|
+
* (display purposes).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import ts from 'typescript'
|
|
25
|
+
import { parseRecordIndexAccess, stringifyParsedExpr } from '@barefootjs/jsx'
|
|
26
|
+
import type { ParsedExpr } from '@barefootjs/jsx'
|
|
27
|
+
|
|
28
|
+
import type { ErbSpreadContext } from '../emit-context.ts'
|
|
29
|
+
import { rubySymbolKey, escapeRubySingleQuoted } from '../lib/ruby-naming.ts'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Lower a `cond ? {…} : {…}` conditional-spread expression — carried as the
|
|
33
|
+
* IR's structured `ParsedExpr` tree — to a Ruby ternary over two Hashes, or
|
|
34
|
+
* null when it isn't that shape. `parseExpression` already strips redundant
|
|
35
|
+
* parentheses, so the conditional / object-literal shapes surface directly.
|
|
36
|
+
*/
|
|
37
|
+
export function conditionalSpreadToRuby(
|
|
38
|
+
ctx: ErbSpreadContext,
|
|
39
|
+
expr: ParsedExpr | undefined,
|
|
40
|
+
): string | null {
|
|
41
|
+
if (!expr || expr.kind !== 'conditional') return null
|
|
42
|
+
const whenTrue = expr.consequent
|
|
43
|
+
const whenFalse = expr.alternate
|
|
44
|
+
if (whenTrue.kind !== 'object-literal' || whenFalse.kind !== 'object-literal') {
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
// Thread the condition's carried `ParsedExpr` tree straight through as
|
|
48
|
+
// `preParsed` — no stringify→re-parse round-trip; `convertExpressionToRuby`
|
|
49
|
+
// uses the tree directly and derives any diagnostic text from it.
|
|
50
|
+
const condRuby = ctx.convertExpressionToRuby('', expr.test)
|
|
51
|
+
const trueRuby = objectLiteralToRubyHash(ctx, whenTrue)
|
|
52
|
+
const falseRuby = objectLiteralToRubyHash(ctx, whenFalse)
|
|
53
|
+
if (trueRuby === null || falseRuby === null) return null
|
|
54
|
+
// JS ternary tests JS truthiness — wrap, mirroring the top-level emitter's
|
|
55
|
+
// `conditional()`.
|
|
56
|
+
return `(bf.truthy?(${condRuby}) ? ${trueRuby} : ${falseRuby})`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Lower a bare object-literal expression (`{ align: 'start' }`), carried as
|
|
61
|
+
* the IR's structured `ParsedExpr` tree, to a Ruby Hash via
|
|
62
|
+
* `objectLiteralToRubyHash`, or null when it isn't a plain object literal.
|
|
63
|
+
* Used for inline object-literal child props (carousel `opts`).
|
|
64
|
+
*/
|
|
65
|
+
export function objectLiteralExprToRubyHash(
|
|
66
|
+
ctx: ErbSpreadContext,
|
|
67
|
+
expr: ParsedExpr | undefined,
|
|
68
|
+
): string | null {
|
|
69
|
+
if (!expr || expr.kind !== 'object-literal') return null
|
|
70
|
+
return objectLiteralToRubyHash(ctx, expr)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Convert a static object literal into a Ruby Hash string for a conditional
|
|
75
|
+
* spread. Only static string/identifier keys are allowed; values resolve
|
|
76
|
+
* via `convertExpressionToRuby`. Returns null for any computed/spread/
|
|
77
|
+
* dynamic key. Empty object → `{}`.
|
|
78
|
+
*/
|
|
79
|
+
export function objectLiteralToRubyHash(
|
|
80
|
+
ctx: ErbSpreadContext,
|
|
81
|
+
obj: Extract<ParsedExpr, { kind: 'object-literal' }>,
|
|
82
|
+
): string | null {
|
|
83
|
+
const entries: string[] = []
|
|
84
|
+
for (const prop of obj.properties) {
|
|
85
|
+
// Shorthand `{ a }` was a `ShorthandPropertyAssignment` (not a
|
|
86
|
+
// `PropertyAssignment`), so the former parser rejected it — keep refusing.
|
|
87
|
+
if (prop.shorthand) return null
|
|
88
|
+
// A numeric key (`{ 1: x }`) was rejected by the former parser (only
|
|
89
|
+
// identifier / string-literal names were accepted); `keyKind`
|
|
90
|
+
// distinguishes it from a same-text string `'1'` key.
|
|
91
|
+
if (prop.keyKind === 'numeric') return null
|
|
92
|
+
const key = prop.key
|
|
93
|
+
const val = prop.value
|
|
94
|
+
const indexed = recordIndexAccessToRuby(ctx, val)
|
|
95
|
+
if (
|
|
96
|
+
indexed === null &&
|
|
97
|
+
val.kind === 'index-access' &&
|
|
98
|
+
!isLiteralIndex(val.index)
|
|
99
|
+
) {
|
|
100
|
+
// Variable-index record access (`sizeMap[size]`) that the
|
|
101
|
+
// static-inline path couldn't resolve — a non-scalar record value,
|
|
102
|
+
// or a non-const receiver. Record BF101 and bail so the whole spread
|
|
103
|
+
// surfaces the out-of-shape diagnostic.
|
|
104
|
+
ctx.errors.push({
|
|
105
|
+
code: 'BF101',
|
|
106
|
+
severity: 'error',
|
|
107
|
+
message: `Spread object value '${stringifyParsedExpr(val)}' indexes a record map whose values aren't scalar literals — it can't lower to an inline Ruby Hash.`,
|
|
108
|
+
loc: { file: ctx.componentName + '.tsx', start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
109
|
+
suggestion: {
|
|
110
|
+
message: 'Index a record whose values are number/string literals, or move the spread into a \'use client\' component so hydration computes it.',
|
|
111
|
+
},
|
|
112
|
+
})
|
|
113
|
+
return null
|
|
114
|
+
}
|
|
115
|
+
const valRuby =
|
|
116
|
+
indexed !== null
|
|
117
|
+
? indexed
|
|
118
|
+
// Thread the carried `val` tree straight through as `preParsed` —
|
|
119
|
+
// no stringify→re-parse round-trip.
|
|
120
|
+
: ctx.convertExpressionToRuby('', val)
|
|
121
|
+
entries.push(`${rubySymbolKey(key)} ${valRuby}`)
|
|
122
|
+
}
|
|
123
|
+
return entries.length === 0 ? '{}' : `{ ${entries.join(', ')} }`
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** True when a parsed index is a numeric or string literal (`arr[0]`, `m['k']`). */
|
|
127
|
+
function isLiteralIndex(index: ParsedExpr): boolean {
|
|
128
|
+
return (
|
|
129
|
+
index.kind === 'literal' &&
|
|
130
|
+
(index.literalType === 'number' || index.literalType === 'string')
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Lower a spread-object VALUE of the form `IDENT[KEY]` where:
|
|
136
|
+
* - `IDENT` resolves via `localConstants` to a MODULE-scope object
|
|
137
|
+
* literal whose property values are all scalar (number/string)
|
|
138
|
+
* literals under static (string-literal or identifier) keys
|
|
139
|
+
* (a `Record<staticKeys, scalar>` map like `sizeMap`), AND
|
|
140
|
+
* - `KEY` is a bare identifier that is a prop.
|
|
141
|
+
* Emits an inline indexed Ruby Hash:
|
|
142
|
+
* `{ sm: 16, md: 20 }[v[:size].to_sym]`
|
|
143
|
+
*
|
|
144
|
+
* Returns the Ruby string when convertible, else `null` so the caller falls
|
|
145
|
+
* back to its normal value lowering (which records BF101 for an unsupported
|
|
146
|
+
* shape). Mirror of the Go adapter's `recordIndexAccessToGoMap`.
|
|
147
|
+
*/
|
|
148
|
+
export function recordIndexAccessToRuby(ctx: ErbSpreadContext, val: ParsedExpr): string | null {
|
|
149
|
+
// `parseRecordIndexAccess` (the shared single-source-of-truth parser) takes a
|
|
150
|
+
// `ts.Expression`. The only shape it accepts is `IDENT[KEY]` with identifier
|
|
151
|
+
// object and index, so rebuild exactly that node from the carried tree via
|
|
152
|
+
// `ts.factory` — no source-text re-parse needed. Any other shape can't match
|
|
153
|
+
// and short-circuits to `null` here.
|
|
154
|
+
if (
|
|
155
|
+
val.kind !== 'index-access' ||
|
|
156
|
+
val.object.kind !== 'identifier' ||
|
|
157
|
+
val.index.kind !== 'identifier'
|
|
158
|
+
) {
|
|
159
|
+
return null
|
|
160
|
+
}
|
|
161
|
+
const tsVal = ts.factory.createElementAccessExpression(
|
|
162
|
+
ts.factory.createIdentifier(val.object.name),
|
|
163
|
+
ts.factory.createIdentifier(val.index.name),
|
|
164
|
+
)
|
|
165
|
+
// Shared structural parse (single source of truth in `@barefootjs/jsx`);
|
|
166
|
+
// this wrapper only does the Ruby-specific emit (Hash literal + symbol
|
|
167
|
+
// key access) from the structured result.
|
|
168
|
+
const parsed = parseRecordIndexAccess(tsVal, ctx.localConstants, ctx.propsParams)
|
|
169
|
+
if (!parsed) return null
|
|
170
|
+
const entries = parsed.entries.map(e => {
|
|
171
|
+
const mapVal =
|
|
172
|
+
e.value.kind === 'number' ? e.value.text : `'${escapeRubySingleQuoted(e.value.text)}'`
|
|
173
|
+
return `${rubySymbolKey(e.key)} ${mapVal}`
|
|
174
|
+
})
|
|
175
|
+
return `{ ${entries.join(', ')} }[v[:${parsed.indexPropName}].to_sym]`
|
|
176
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* String-type value helpers for the ERB template adapter.
|
|
3
|
+
*
|
|
4
|
+
* Ported from the Mojolicious adapter's `value/parsed-literal.ts`
|
|
5
|
+
* (issue #2018 track D lineage). Pure functions over analyzer type info /
|
|
6
|
+
* const-initializer text — no adapter instance state.
|
|
7
|
+
*
|
|
8
|
+
* SHARED CANDIDATE: `isStringTypeInfo` and `isBareStringLiteral` are
|
|
9
|
+
* byte-identical to the Mojo/Xslate adapters' copies and adapter-agnostic.
|
|
10
|
+
*
|
|
11
|
+
* Module-scope pure-string-const inlining is owned by the shared
|
|
12
|
+
* `collectModuleStringConsts` in `@barefootjs/jsx` (consumed via
|
|
13
|
+
* `moduleStringConsts`), so no adapter-local source re-parse lives here.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { TypeInfo } from '@barefootjs/jsx'
|
|
17
|
+
|
|
18
|
+
/** True when `type` is the `string` primitive. */
|
|
19
|
+
export function isStringTypeInfo(type: TypeInfo | undefined): boolean {
|
|
20
|
+
return type?.kind === 'primitive' && type.primitive === 'string'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** True when `initialValue` is a bare string-literal expression (`'x'` /
|
|
24
|
+
* `"x"`), used as a fallback for signals whose type wasn't inferred. */
|
|
25
|
+
export function isBareStringLiteral(initialValue: string | undefined): boolean {
|
|
26
|
+
if (!initialValue) return false
|
|
27
|
+
const v = initialValue.trim()
|
|
28
|
+
return (v.startsWith("'") && v.endsWith("'")) || (v.startsWith('"') && v.endsWith('"'))
|
|
29
|
+
}
|
package/src/build.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// ERB build config factory for barefoot.config.ts
|
|
2
|
+
|
|
3
|
+
import type { BuildOptions } from '@barefootjs/jsx'
|
|
4
|
+
import { ErbAdapter } from './adapter/index.ts'
|
|
5
|
+
import type { ErbAdapterOptions } from './adapter/index.ts'
|
|
6
|
+
|
|
7
|
+
export interface ErbBuildOptions extends BuildOptions {
|
|
8
|
+
/** Adapter-specific options passed to ErbAdapter */
|
|
9
|
+
adapterOptions?: ErbAdapterOptions
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a BarefootBuildConfig for ERB (Embedded Ruby) template projects.
|
|
14
|
+
*
|
|
15
|
+
* Uses structural typing — does not import BarefootBuildConfig to avoid a
|
|
16
|
+
* circular dependency between @barefootjs/erb and @barefootjs/cli.
|
|
17
|
+
*/
|
|
18
|
+
export function createConfig(options: ErbBuildOptions = {}) {
|
|
19
|
+
return {
|
|
20
|
+
adapter: new ErbAdapter(options.adapterOptions),
|
|
21
|
+
paths: options.paths,
|
|
22
|
+
components: options.components,
|
|
23
|
+
outDir: options.outDir,
|
|
24
|
+
minify: options.minify,
|
|
25
|
+
contentHash: options.contentHash,
|
|
26
|
+
externals: options.externals,
|
|
27
|
+
externalsBasePath: options.externalsBasePath,
|
|
28
|
+
bundleEntries: options.bundleEntries,
|
|
29
|
+
localImportPrefixes: options.localImportPrefixes,
|
|
30
|
+
outputLayout: options.outputLayout ?? {
|
|
31
|
+
templates: 'templates',
|
|
32
|
+
clientJs: 'client',
|
|
33
|
+
runtime: 'client',
|
|
34
|
+
},
|
|
35
|
+
postBuild: options.postBuild,
|
|
36
|
+
}
|
|
37
|
+
}
|