@barefootjs/erb 0.18.0 → 0.18.2

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/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # barefoot_js
2
+
3
+ Ruby runtime for [BarefootJS](https://barefootjs.dev/) marked templates, targeting ERB.
4
+
5
+ [BarefootJS](https://github.com/piconic-ai/barefootjs) is a fine-grained reactive TSX compiler: you write components in TSX, and the compiler emits templates for your backend's template engine plus the client-side JS that hydrates them. This gem is the server half for Ruby — it renders the `.erb` templates produced by the `@barefootjs/erb` adapter.
6
+
7
+ ## Installation
8
+
9
+ ```sh
10
+ gem install barefoot_js
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ Every compiled `.erb` template receives a `BarefootJS::Context` as the `bf` local. The context is engine-agnostic; everything that depends on how a template is rendered is delegated to a pluggable backend (`BarefootJS::Backend::Erb` is the ERB reference implementation):
16
+
17
+ ```ruby
18
+ require 'barefoot_js'
19
+
20
+ bf = BarefootJS::Context.new(backend)
21
+ ```
22
+
23
+ Values are JSON-shaped Ruby data with symbol hash keys throughout (props, env hashes, array-of-hash records).
24
+
25
+ ## Documentation
26
+
27
+ - [barefootjs.dev](https://barefootjs.dev/) — core documentation
28
+ - [GitHub: piconic-ai/barefootjs](https://github.com/piconic-ai/barefootjs) — monorepo (this gem lives at `packages/adapter-erb`)
29
+
30
+ ## License
31
+
32
+ MIT
@@ -1 +1 @@
1
- {"version":3,"file":"conformance-pins.d.ts","sourceRoot":"","sources":["../src/conformance-pins.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAEtD,eAAO,MAAM,eAAe,EAAE,eAsF7B,CAAA"}
1
+ {"version":3,"file":"conformance-pins.d.ts","sourceRoot":"","sources":["../src/conformance-pins.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAEtD,eAAO,MAAM,eAAe,EAAE,eAiG7B,CAAA"}
package/dist/index.d.ts CHANGED
@@ -6,4 +6,5 @@
6
6
  export { ErbAdapter, erbAdapter } from './adapter/index.ts';
7
7
  export type { ErbAdapterOptions } from './adapter/index.ts';
8
8
  export { conformancePins } from './conformance-pins.ts';
9
+ export { renderDivergences } from './render-divergences.ts';
9
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAC3D,YAAY,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAC3D,YAAY,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAA"}
package/dist/index.js CHANGED
@@ -189163,9 +189163,28 @@ var conformancePins = {
189163
189163
  "filter-nested-find-predicate": [
189164
189164
  { code: "BF101", severity: "error", issue: "https://github.com/piconic-ai/barefootjs/issues/2038" }
189165
189165
  ],
189166
- "array-map-function-reference": [{ code: "BF101", severity: "error" }]
189166
+ "array-map-function-reference": [{ code: "BF101", severity: "error" }],
189167
+ "dangerous-inner-html": [{ code: "BF101", severity: "error" }],
189168
+ "string-replaceall": [{ code: "BF101", severity: "error" }]
189169
+ };
189170
+ // src/render-divergences.ts
189171
+ var renderDivergences = {
189172
+ "falsy-text-values": '`{false}` renders "false" (Hono drops it); `{null}`/`{undefined}` render empty (Hono renders "null") — neither side matches JSX semantics',
189173
+ "html-entity-text": "`©` in JSX literal text: Hono decodes to `©`, this adapter re-emits the raw entity — same DOM, different bytes",
189174
+ "optional-chaining-prop": "`user?.name ?? …` on an object prop: the Ruby render exits 1 (optional chaining into a Hash prop has no lowering)",
189175
+ "math-methods": "Math.min/max/abs over a signal render empty (only Math.floor is in the template-primitive registry)",
189176
+ "boolean-attr-literals": 'camelCase boolean alias `readOnly`: Hono SSRs `readOnly="true"`, this adapter emits bare presence',
189177
+ "camelcase-attributes": "`htmlFor` is not lowered to `for` (Hono maps it)",
189178
+ "static-attr-escape": 'static attribute values are not HTML-escaped (`title="Fish & Chips"` emitted raw; Hono escapes)',
189179
+ "svg-icon": "SVG camelCase presentation attrs (`strokeWidth`, `strokeLinecap`) pass through unmapped; Hono lowers to kebab-case",
189180
+ "object-entries-map": "`Object.entries(prop).map(([k, v]) => …)` renders but its loop item keys diverge from the reference serialisation",
189181
+ "nested-loop-outer-binding": "nested-loop inner items carry `data-key` where the reference emits the depth-suffixed `data-key-1`",
189182
+ "jsx-element-prop": "a JSX element passed as a NON-children prop renders an empty slot — the element value is silently dropped",
189183
+ "string-slice": '`.slice()` on a STRING lowers through the array slice helper and renders "[]" instead of the substring',
189184
+ "string-trim-sided": "`.trimStart()` / `.trimEnd()` render empty (no lowering; only both-sides `.trim` is wired)"
189167
189185
  };
189168
189186
  export {
189187
+ renderDivergences,
189169
189188
  erbAdapter,
189170
189189
  conformancePins,
189171
189190
  ErbAdapter
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Render-level divergences against the shared conformance corpus
3
+ * (Priority-12 edge-case sweep, #2168): fixtures that COMPILE clean on
4
+ * this adapter but whose rendered output diverges from the Hono
5
+ * reference on real Ruby erb (or fails at render time).
6
+ *
7
+ * Consumed by this package's conformance test (its `skipJsx` set is
8
+ * derived from these keys, so the skip list and this declaration can't
9
+ * drift) and by `packages/compat`, which publishes the entries in the
10
+ * fixture-divergences section of `ui/compat.lock.json` — surfaced on
11
+ * the docs compatibility-matrix page. Graduating an entry means fixing
12
+ * the adapter (or the shared compiler layer) and deleting the line.
13
+ */
14
+ import type { RenderDivergences } from '@barefootjs/jsx';
15
+ export declare const renderDivergences: RenderDivergences;
16
+ //# sourceMappingURL=render-divergences.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-divergences.d.ts","sourceRoot":"","sources":["../src/render-divergences.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AAExD,eAAO,MAAM,iBAAiB,EAAE,iBA2B/B,CAAA"}
@@ -324,7 +324,15 @@ module BarefootJS
324
324
  extra[name] = props.key?(name) ? props[name] : d[:value]
325
325
  next
326
326
  end
327
- prop_name = d[:propName]
327
+ # `propName` rides in from the JSON manifest as a String -- JSON has
328
+ # no symbol type, and the manifest's `symbolize_names: true` parse
329
+ # only symbolizes hash KEYS, never string values. Runtime prop
330
+ # hashes, meanwhile, are symbol-keyed because compiled ERB templates
331
+ # pass `{ children: ... }` literals, so `props.key?(prop_name)` with
332
+ # the String would always miss, silently falling back to the static
333
+ # default for every manifest-registered child (e.g. `children`
334
+ # rendering empty) (#2157).
335
+ prop_name = d[:propName]&.to_sym
328
336
  extra[name] =
329
337
  if !prop_name.nil? && props.key?(prop_name) && !props[prop_name].nil?
330
338
  props[prop_name]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/erb",
3
- "version": "0.18.0",
3
+ "version": "0.18.2",
4
4
  "description": "ERB (Embedded Ruby) adapter for BarefootJS — compiles IR to .erb templates and ships the Ruby rendering backend; runs under any Rack app (Sinatra, Rails)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -54,14 +54,14 @@
54
54
  "directory": "packages/adapter-erb"
55
55
  },
56
56
  "dependencies": {
57
- "@barefootjs/shared": "0.18.0"
57
+ "@barefootjs/shared": "0.18.2"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "@barefootjs/jsx": ">=0.2.0"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@barefootjs/adapter-tests": "0.1.0",
64
- "@barefootjs/jsx": "0.18.0",
64
+ "@barefootjs/jsx": "0.18.2",
65
65
  "typescript": "^5.0.0"
66
66
  }
67
67
  }
@@ -26,11 +26,19 @@ import { ErbAdapter } from '../adapter'
26
26
  import { renderErbComponent, ErbNotAvailableError } from '../test-render'
27
27
  import { compileJSX, type ComponentIR } from '@barefootjs/jsx'
28
28
  import { conformancePins } from '../conformance-pins'
29
+ import { renderDivergences } from '../render-divergences'
29
30
 
30
31
  runAdapterConformanceTests({
31
32
  name: 'erb',
32
33
  factory: () => new ErbAdapter(),
33
34
  render: renderErbComponent,
35
+ // Priority-12 edge-case sweep (炙り出し, #2168): render-level
36
+ // divergences are declared in `../render-divergences` (exported from the
37
+ // package index and published to `ui/compat.lock.json` / the docs
38
+ // compatibility-matrix page by `packages/compat`). Deriving the skip
39
+ // list from that object keeps the public declaration and these test
40
+ // skips from drifting; each entry's rationale lives there.
41
+ skipJsx: Object.keys(renderDivergences),
34
42
  // No JSX-render skips: every shared conformance fixture — including
35
43
  // the composed `site/ui` demo corpus (#1467 / #1897) — renders to
36
44
  // Hono parity on real Ruby `erb`. `data-table` came off via the
@@ -97,4 +97,15 @@ export const conformancePins: ConformancePins = {
97
97
  // with BF101 rather than emitting a broken template. Same pin as
98
98
  // mojo/xslate.
99
99
  'array-map-function-reference': [{ code: 'BF101', severity: 'error' }],
100
+ // Edge-case sweep (Priority 12): `dangerouslySetInnerHTML` requires a
101
+ // deliberate raw-HTML (unescaped) output affordance in the target
102
+ // template language. No lowering exists yet, so the compiler refuses
103
+ // the shape loudly instead of emitting entity-escaped markup that
104
+ // silently renders tags as text.
105
+ 'dangerous-inner-html': [{ code: 'BF101', severity: 'error' }],
106
+ // Edge-case sweep (Priority 12): `.replaceAll` has no lowering yet —
107
+ // only first-occurrence `.replace` is wired to the runtime helpers.
108
+ // Refused with BF101 rather than reusing the first-only lowering,
109
+ // which would silently change semantics.
110
+ 'string-replaceall': [{ code: 'BF101', severity: 'error' }],
100
111
  }
package/src/index.ts CHANGED
@@ -7,3 +7,4 @@
7
7
  export { ErbAdapter, erbAdapter } from './adapter/index.ts'
8
8
  export type { ErbAdapterOptions } from './adapter/index.ts'
9
9
  export { conformancePins } from './conformance-pins.ts'
10
+ export { renderDivergences } from './render-divergences.ts'
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Render-level divergences against the shared conformance corpus
3
+ * (Priority-12 edge-case sweep, #2168): fixtures that COMPILE clean on
4
+ * this adapter but whose rendered output diverges from the Hono
5
+ * reference on real Ruby erb (or fails at render time).
6
+ *
7
+ * Consumed by this package's conformance test (its `skipJsx` set is
8
+ * derived from these keys, so the skip list and this declaration can't
9
+ * drift) and by `packages/compat`, which publishes the entries in the
10
+ * fixture-divergences section of `ui/compat.lock.json` — surfaced on
11
+ * the docs compatibility-matrix page. Graduating an entry means fixing
12
+ * the adapter (or the shared compiler layer) and deleting the line.
13
+ */
14
+
15
+ import type { RenderDivergences } from '@barefootjs/jsx'
16
+
17
+ export const renderDivergences: RenderDivergences = {
18
+ 'falsy-text-values':
19
+ '`{false}` renders "false" (Hono drops it); `{null}`/`{undefined}` render empty (Hono renders "null") — neither side matches JSX semantics',
20
+ 'html-entity-text':
21
+ '`©` in JSX literal text: Hono decodes to `©`, this adapter re-emits the raw entity — same DOM, different bytes',
22
+ 'optional-chaining-prop':
23
+ '`user?.name ?? …` on an object prop: the Ruby render exits 1 (optional chaining into a Hash prop has no lowering)',
24
+ 'math-methods':
25
+ 'Math.min/max/abs over a signal render empty (only Math.floor is in the template-primitive registry)',
26
+ 'boolean-attr-literals':
27
+ 'camelCase boolean alias `readOnly`: Hono SSRs `readOnly="true"`, this adapter emits bare presence',
28
+ 'camelcase-attributes':
29
+ '`htmlFor` is not lowered to `for` (Hono maps it)',
30
+ 'static-attr-escape':
31
+ 'static attribute values are not HTML-escaped (`title="Fish & Chips"` emitted raw; Hono escapes)',
32
+ 'svg-icon':
33
+ 'SVG camelCase presentation attrs (`strokeWidth`, `strokeLinecap`) pass through unmapped; Hono lowers to kebab-case',
34
+ 'object-entries-map':
35
+ '`Object.entries(prop).map(([k, v]) => …)` renders but its loop item keys diverge from the reference serialisation',
36
+ 'nested-loop-outer-binding':
37
+ 'nested-loop inner items carry `data-key` where the reference emits the depth-suffixed `data-key-1`',
38
+ 'jsx-element-prop':
39
+ 'a JSX element passed as a NON-children prop renders an empty slot — the element value is silently dropped',
40
+ 'string-slice':
41
+ '`.slice()` on a STRING lowers through the array slice helper and renders "[]" instead of the substring',
42
+ 'string-trim-sided':
43
+ '`.trimStart()` / `.trimEnd()` render empty (no lowering; only both-sides `.trim` is wired)',
44
+ }
@@ -22,7 +22,18 @@
22
22
  * Child components are wired through the production
23
23
  * `BarefootJS::Context#register_child_renderer` so the child's `bf-s` scope
24
24
  * id derives from `<parentScope>_<slotId>` exactly as a real `bf build`
25
- * page would.
25
+ * page would. As of #2158, the child-vars seeding step also runs the exact
26
+ * production sequence — `BarefootJS::Context.derive_vars_from_defaults`,
27
+ * the same call `register_components_from_manifest` makes
28
+ * (`lib/barefoot_js.rb` ~lines 283-292) — instead of a harness-local
29
+ * `defaults.merge(child_props)` shortcut. That shortcut is why the #2157
30
+ * bug (a manifest `propName` riding in as a JSON String, looked up against
31
+ * symbol-keyed runtime prop Hashes, so `children` silently fell back to
32
+ * the static default and rendered empty) was invisible to this whole
33
+ * conformance corpus: the harness never exercised the code path the bug
34
+ * lived in. Now it does, so the same class of regression is caught by
35
+ * every fixture that renders a child component, not just a dedicated
36
+ * regression test.
26
37
  */
27
38
 
28
39
  import { compileJSX, extractSsrDefaults, importsSearchParams } from '@barefootjs/jsx'
@@ -255,12 +266,21 @@ export async function renderErbComponent(options: RenderOptions): Promise<string
255
266
  const rootScopeIdRaw = typeof props?.__instanceId === 'string' ? props.__instanceId : 'test'
256
267
  const rootScopeId = rubyStringLiteral(rootScopeIdRaw)
257
268
 
258
- // Static ssrDefaults per child, simplified to bare values (the
259
- // caller's props always win see `buildChildRenderersRuby`) and
260
- // written as one JSON file keyed by snake_case template name.
261
- const childDefaults: Record<string, Record<string, unknown>> = {}
269
+ // Static ssrDefaults per child, written VERBATIM `propName` /
270
+ // `isRestProps` / `value` intact, exactly the shape the production
271
+ // build manifest embeds — and written as one JSON file keyed by
272
+ // snake_case template name. `buildChildRenderersRuby` below feeds this
273
+ // straight into `BarefootJS::Context.derive_vars_from_defaults`, the
274
+ // same call `register_components_from_manifest` makes in the published
275
+ // gem (#2158). A prior version of this harness simplified each entry
276
+ // to its bare `value` and did `defaults.merge(child_props)` directly —
277
+ // that sidestepped `derive_vars_from_defaults`'s `propName` lookup
278
+ // entirely, which is exactly why the #2157 String-vs-symbol `propName`
279
+ // bug (children rendering empty) was invisible to this whole
280
+ // conformance corpus.
281
+ const childDefaults: Record<string, Record<string, SsrDefault>> = {}
262
282
  for (const [childName, { ir: childIR }] of childTemplates) {
263
- childDefaults[toSnakeCase(childName)] = simplifySsrDefaults(extractSsrDefaults(childIR.metadata) ?? {})
283
+ childDefaults[toSnakeCase(childName)] = extractSsrDefaults(childIR.metadata) ?? {}
264
284
  }
265
285
  if (childTemplates.size > 0) {
266
286
  await Bun.write(resolve(tempDir, 'child_defaults.json'), JSON.stringify(childDefaults))
@@ -343,10 +363,19 @@ function collectImportedComponentNames(ir: ComponentIR): string[] {
343
363
  * derives the child scope id from `<parentScope>_<slotId>` (the parent's
344
364
  * `bf.render_child('<name>', { …, _bf_slot: '<slotId>' })` passes
345
365
  * `_bf_slot`), seeds signal / memo / prop defaults from the child IR's
346
- * `ssrDefaults` (loaded once from `child_defaults.json`, caller props win
347
- * via `Hash#merge`), shares the parent's script list, and renders the
348
- * child `.erb` through the same backend. Loop children (no `_bf_slot`)
349
- * fall back to `<ComponentName>_<rand>` like the Perl harnesses.
366
+ * `ssrDefaults` (loaded once from `child_defaults.json`) THROUGH THE SAME
367
+ * `BarefootJS::Context.derive_vars_from_defaults` call
368
+ * `register_components_from_manifest` makes (lib/barefoot_js.rb ~lines
369
+ * 283-292) not a harness-local re-derivation — so the propName /
370
+ * isRestProps resolution the published gem actually runs is exactly what
371
+ * this harness exercises (#2158; this is the fix for the #2157 class of
372
+ * bug, where a harness-local `defaults.merge(child_props)` shortcut never
373
+ * touched that code path and so never caught the String-vs-symbol
374
+ * `propName` mismatch). Caller props still win over static defaults —
375
+ * that's `derive_vars_from_defaults`'s own contract — shares the parent's
376
+ * script list, and renders the child `.erb` through the same backend.
377
+ * Loop children (no `_bf_slot`) fall back to `<ComponentName>_<rand>`
378
+ * like the Perl harnesses.
350
379
  */
351
380
  function buildChildRenderersRuby(
352
381
  childTemplates: Map<string, { template: string; ir: ComponentIR }>,
@@ -411,8 +440,16 @@ function buildChildRenderersRuby(
411
440
  lines.push(` child_bf._child_renderers(bf._child_renderers)`)
412
441
  lines.push(` child_bf._scripts(bf._scripts)`)
413
442
  lines.push(` child_bf._script_seen(bf._script_seen)`)
414
- // Seed template vars: static ssrDefaults first, caller's props win.
415
- lines.push(` vars = defaults_${snakeName}.merge(child_props)`)
443
+ // Seed template vars through the production sequence the exact
444
+ // call `register_components_from_manifest` makes in the published gem
445
+ // (lib/barefoot_js.rb ~lines 283-292) — instead of a harness-local
446
+ // `defaults.merge(child_props)` shortcut. `derive_vars_from_defaults`
447
+ // resolves each entry's `propName` (symbolized) against the live
448
+ // `child_props`, falling back to the static `value`; `isRestProps`
449
+ // entries pass the rest bag through. Caller props still win overall
450
+ // via the final `merge`.
451
+ lines.push(` extra = BarefootJS::Context.derive_vars_from_defaults(defaults_${snakeName}, child_props)`)
452
+ lines.push(` vars = child_props.merge(extra)`)
416
453
  lines.push(` rendered = backend.render_named(${rubyStringLiteral(snakeName)}, child_bf, vars)`)
417
454
  lines.push(` rendered.chomp`)
418
455
  lines.push(`end)`)
@@ -430,23 +467,6 @@ function rubySymbol(name: string): string {
430
467
  return /^[A-Za-z_][A-Za-z0-9_]*[?!]?$/.test(name) ? `:${name}` : `:"${name.replace(/"/g, '\\"')}"`
431
468
  }
432
469
 
433
- /**
434
- * Simplify an `extractSsrDefaults` map to bare values for the child
435
- * defaults JSON file. Each entry is either a bare value or
436
- * `{ value, propName?, isRestProps? }`; the child renderer always merges
437
- * the caller's live `child_props` OVER these defaults (`Hash#merge`), so
438
- * only the static fallback `value` is needed here — the `propName`-aware
439
- * resolution the production manifest path does is redundant with that
440
- * merge.
441
- */
442
- function simplifySsrDefaults(defaults: Record<string, SsrDefault>): Record<string, unknown> {
443
- const out: Record<string, unknown> = {}
444
- for (const [name, d] of Object.entries(defaults)) {
445
- out[name] = d.value
446
- }
447
- return out
448
- }
449
-
450
470
  /**
451
471
  * Convert PascalCase to snake_case for template naming (matches the
452
472
  * adapter's `toTemplateName`).